﻿using System;
using System.Globalization;
using System.Reflection;
using FluentNHibernate;
using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.Inspections;
using FluentNHibernate.Conventions.Instances;
using NHibernate.Sidekick.Utils.Extensions;

namespace NHibernate.Sidekick.Utils.MappingConventions
{
    /// <summary>
    /// Sets the many-to-many entity names with the convention "{FirstTableName_SecondTableName}".
    /// Additionally, it truncates the name to 30 characters to avoid the Oracle error
    /// 'ORA-00972: identifier is too long' by truncating names longer than 30 characters.'
    /// </summary>
    public class OracleNamingConvention : ManyToManyTableNameConvention, IClassConvention, IPropertyConvention
    {
        /// <summary>
        /// Apply changes to the target.
        /// </summary>
        /// <param name="instance"></param>
        public void Apply(IClassInstance instance)
        {
            instance.Table(OracleConventionSetter.ApplyOracleNamingConventions(instance.EntityType.Name));
        }

        /// <summary>
        /// Apply changes to the target.
        /// </summary>
        /// <param name="instance"></param>
        public void Apply(IPropertyInstance instance)
        {
            instance.Column(OracleConventionSetter.ApplyOracleNamingConventions(instance.Property.Name));
        }

        /// <summary>
        /// Controls how the join table is named for bi-directional relationships ("{MainTable}_{InverseCollection}").
        /// </summary>
        /// <param name="collection">Main collection.</param>
        /// <param name="otherSide">Inverse collection.</param>
        /// <returns>Many-to-many table name.</returns>
        /// <remarks>
        /// This method will be called once per bi-directional relationship; once one side of the relationship
        /// has been saved, then the other side will assume that name aswell.
        /// </remarks>
        protected override string GetBiDirectionalTableName(IManyToManyCollectionInspector collection, IManyToManyCollectionInspector otherSide)
        {
            var name = string.Format(CultureInfo.InvariantCulture, "{0}_{1}",
                                     collection.EntityType.Name, otherSide.EntityType.Name);

            return OracleConventionSetter.ApplyOracleNamingConventions(name);
        }

        /// <summary>
        /// Controls how the join table is named for uni-directional relationships ("{MainTable}_{ChildTable}").
        /// </summary>
        /// <param name="collection">Main collection.</param>
        /// <returns>Many-to-many table name.</returns>
        protected override string GetUniDirectionalTableName(IManyToManyCollectionInspector collection)
        {
            var name = string.Format(CultureInfo.InvariantCulture, "{0}_{1}",
                                     collection.EntityType.Name, collection.ChildType.Name);

            return OracleConventionSetter.ApplyOracleNamingConventions(name);
        }
    }

    /// <summary>
    /// Sets a table's foreign key names with the convention "Id_{TableName}".
    /// Additionally, it truncates the name to 30 characters to avoid the Oracle error
    /// 'ORA-00972: identifier is too long' by truncating names longer than 30 characters.'
    /// </summary>
    public class OracleForeignKeyConvention : FluentNHibernate.Conventions.ForeignKeyConvention
    {
        /// <summary>
        /// Gets the name of the key.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        protected override string GetKeyName(Member property, System.Type type)
        {
            var name = property == null
                       ? "Id_" + type.Name.ToUnderscoredNaming()
                       : "Id_" + property.Name.ToUnderscoredNaming();

            return name.Truncate(30);
        }
    }

    /// <summary>
    /// Reformats the string from camelCase to an Under_Scored convention and
    /// truncates the string to 30 characters.
    /// </summary>
    internal static class OracleConventionSetter
    {
        public static string ApplyOracleNamingConventions(string text)
        {
            return text.ToUnderscoredNaming().Truncate(30);
        }
    }
}